1. Intro

이번에는 leafletcrosstalk을 활용하여 지도 시각화 및 위젯 간 상호작용을 다뤄볼 것입니다. leaflet은 지도 시각화를 위한 자바스크립트의 오픈소스 라이브러리 Leaflet.js의 R 인터페이스입니다. crosstalk은 하나의 공유 데이터를 중심으로 여러 위젯들을 연결하며, linked brushing, filtering 등의 기능을 제공합니다. 이번 실습에서 사용하는 crosstalk, leaflet, d3scatter 패키지는 아래 깃허브에서 받아주시기 바랍니다.

devtools::install_github("dmurdoch/leaflet@crosstalk4")
devtools::install_github("rstudio/leaflet")
devtools::install_github("jcheng5/d3scatter")

2. Leaflet

2.1. 지도 그리기

지도를 생성하는 코드는 매우 간단하며 직관적입니다. leaflet() %>% addTiles()로 지도를 그리고 mymap이라는 변수에 저장하였습니다. 왼쪽 지도는 이 결과를 그대로 출력한 것이고, 오른쪽 지도는 setView() 함수로 좌표와 확대를 지정하여 출력한 것입니다. crosstalk::bscols() 함수는 단순히 지도를 나란히 배열하기 위해서 사용하였으며, 지도에는 아무런 영향을 미치지 않습니다.

library(tidyverse)
library(leaflet)
mymap = leaflet() %>% addTiles()
crosstalk::bscols(
  mymap,
  mymap %>% setView(126.982, 37.5502, zoom=10)
)

2.2. 지도에 점 찍기

leaflet을 통해서 손쉽게 지도를 그릴 수 있을 뿐 아니라, 지도 위에 데이터를 표현하는 것도 가능합니다. 다음은 지도 위에 점을 표현하는 예제들입니다. 위경도로 표현된 좌표가 있다면 손쉽게 데이터를 지도 위에 표현할 수 있습니다.

points1 = mymap %>% 
  addMarkers(lng=c(126.882, 126.982, 127.082),
             lat=c(37.4502, 37.5502, 37.6502),
             label=c('예시1','예시2','예시3'))

points2 = mymap %>%
  addMarkers(data = quakes[1:20,], lng=~long, lat=~lat, label=~as.character(mag))

crosstalk::bscols(points1,points2)

2.2. 지도에 도형 그리기

지리 데이터는 .shp, .json, .csv 등 다양한 형태로 존재합니다. 이번에는 .json 형식을 파일을 가지고 실습을 진행해보겠습니다. 실습에 사용할 데이터는 2016년 국회의원 총선의 서울 지역 개표결과 데이터입니다. 전처리 geojsonio패키지를 통해서 SpatialPolygonsDataFrame 클래스로 파일을 읽어옵니다. SpatialPolygonsDataFrame은 좌표계, 폴리곤 좌표 등의 지리정보와 일반적인 데이터프레임을 결합해놓은 클래스입니다. election@polygons를 통해서 폴리곤 리스트에 접근할 수 있고, election@data를 통해서 데이터프레임에 접근할 수 있습니다.

election  = geojsonio::geojson_read("data/election.json", what = "sp")

cat("class(election) : ", class(election))
## class(election) :  SpatialPolygonsDataFrame
cat("class(election@polygons) : ", class(election@polygons))
## class(election@polygons) :  list
cat("class(election@data) : ", class(election@data))
## class(election@data) :  data.frame
# 데이터프레임에 간단한 전처리를 해준 예시
colnames(election@data) = colnames(election@data) %>% str_replace(" ","")

지도에 폴리곤을 표현하는 것은 점을 찍는 일만큼이나 간단합니다. 왼쪽 예제에서 볼 수 있듯이 addPolygons()에 데이터를 전달해주는 것만로 폴리곤을 추가할 수 있습니다. 원하는 변수를 전달하면 간단한 라벨을 표현하는 것도 가능합니다. 오른쪽 예제에서는 경계선과 폴리곤 내부 색, 하이라이트를 추가하고, 라벨을 수정하고, 선거구별 폴리라인과 범례를 추가적으로 겹쳐주었습니다. add~ 함수들이 하나의 레이어를 공유한다면, leaflet()안에 먼저 데이터를 전달하는 것도 좋습니다.

poly1 = mymap %>% addPolygons(data=election,label=~당선)

# 지도에 색을 입히기 위해서 파레트 생성
pal = colorFactor(c("#0D7440", "#2A88C5",  "#C10D0D"), c("국민의당", "더불어민주당","새누리당"))

# 툴팁에 담을 정보 생성
labs = lapply(seq(nrow(election@data)), function(i) {
  paste0( '<p><b>', election@data[i, "선거구"]," : ",election@data[i, "읍면동명"], '</b></p><p>', 
          "<b>당선 : ", election@data[i, "당선"], '</b><p></p>', 
          "국민의당 : ", election@data[i, "국민의당"],'</p><p>', 
          "더불어민주당 : ", election@data[i, "더불어민주당"], '</p><p>',
          "새누리당 : ", election@data[i, "새누리당"], '</p>') 
})

# 선거구 경계 생성
election_gu = maptools::unionSpatialPolygons(election, election$선거구)

poly2 = leaflet(election) %>%
  addTiles %>%
  addPolygons(
    # 폴리곤 내부
    fillColor = ~pal(당선), fillOpacity = 0.5,
    # 폴리곤 경계
    weight = 1, opacity = 1, color = "white", dashArray=3,
    # 라벨
    label=lapply(labs, htmltools::HTML),
    # 하이라이트
    highlightOptions = highlightOptions(
      color = "#00ff00", opacity = 1, weight = 2, fillOpacity = 1,
      bringToFront = T, sendToBack = T))%>%
  addLegend(pal = pal, values = ~당선, opacity = 0.7, title = NULL, position = "bottomright") %>%
  addPolylines(
    data=election_gu,
    weight = 1.5,
    opacity = 1,
    color = "black")

crosstalk::bscols(poly1, poly2)

3. Crosstalk

3.1. SharedData

crosstalk은 하나의 공유 데이터를 바탕으로 여러 위젯을 연동시킵니다. SharedData$new() 에 원하는 데이터를 전달하면 공유 데이터가 만들어집니다. 이 결과물을 각각의 위젯에 전달하기만 하면 위젯 간의 연동이 이루어집니다.

library(crosstalk)
shared_elec = SharedData$new(election@data)
class(shared_elec)
## [1] "SharedData" "R6"

SharedDatakeygroup이라는 두 가지 파라미터를 가집니다. 키는 한 줄의 데이터가 갖는 고유한 ID 문자열이며, SharedData$key()를 통해 키를 확인할 수 있습니다. 한 데이터 안에 존재하는 모든 행을 고유하게 식별할 수 있는 컬럼이라면 SharedData의 키가 될 수 있습니다. 좋은 키는 다음의 조건들을 만족합니다.

  • 변하지 않는다
  • 상대적으로 짧다
  • 빈 문자열, NA, NULL이 아니다
  • 중복되지 않는다

만약 SharedData를 생성할 때 키가 명시적으로 전달되지 않았다면, 행 이름row.names이 키로 사용됩니다. 만약 행 이름이 위의 키가 될 수 없는 경우라면, 행 번호가 키로 사용됩니다. 행 번호는 데이터를 정렬하거나 필터링하면 변하게 되므로 좋은 키는 아닙니다. 하지만 샤이니를 사용하거나 그룹을 이용해 특별한 작업을 하는 등의 경우가 아니라면 행 번호로도 충분합니다. 키 인자는 다양한 형식으로 전달될 수 있습니다. 다음은 SharedData에 키를 전달하는 예시입니다.

sd1 = SharedData$new(election@data, ~id)
sd2 = SharedData$new(election@data, election$id)
sd3 = SharedData$new(election@data, function(data) data$id)

all(sd1$key()==sd2$key() & sd2$key()==sd3$key())
## [1] TRUE

모든 SharedData는 하나의 그룹에 속합니다. 위젯들은 자신이 속한 그룹 내의 위젯들과 연동되며, 다른 그룹의 위젯들과는 연동되지 않습니다. 하나의 그룹에 속하는 SharedData 객체들이 반드시 모두 같을 필요는 없으며, 동일한 키와 데이터 포인트를 공유하기만 하면 됩니다. 그룹에 대해서는 이후 예제에서 더 자세히 다루도록 하겠습니다.

crosstalk의 주요한 한계점들은 다음과 같습니다.

  1. 저작자가 직접 HTML 위젯을 적절히 수정해야 한다. crosstalk은 스스로 위젯들을 연동시켜주지는 않는다.

  2. Crosstalk은 개별 데이터 포인트에 대해서만 작동하며, 데이터의 결합이나 요약을 표현하는 위젯들에는 작동하지 않는다. 예를 들어서 개별 데이터 포인트를 표현하는 산점도에는 적용될 수 있지만, 히스토그램에는 적용될 수 없다.

  3. crosstalk큰 데이터에 적합하지 않다. 모든 데이터가 브라우저에 로드되어야 하기 때문이다.

3.1. Linked Brushing

Linked brushing은 하나의 플롯에서 발생한 브러싱을 연결된 다른 플롯들에도 적용하는 기능입니다. 먼저 crosstalk::SharedData로 데이터프레임을 감싸 여러 위젯들을 연결할 공유 데이터를 생성해줍니다. 이 객체를 이용해서 두 개의 그래프를 그렸습니다. 이후 subplot 함수로 두 개의 그래프를 묶어주고, highlight"plotly_selected"를 전달해주면 됩니다. 한 쪽 플롯에서 점들을 선택하면 다른 쪽 플롯에서도 하이라이트되는 것을 확인할 수 있습니다. crosstalkSharedData가 아닌 일반 데이터프레임으로 플롯을 그린다면 당연히 이 기능은 작동하지 않습니다.

library(crosstalk)
library(plotly)

# election@data를 SharedData로 감싸 공유 데이터를 생성
shared_elec = SharedData$new(election@data, ~id)

p1 = plot_ly(shared_elec, x = ~소득, y = ~노령화지수, legendgroup=~당선) %>%
  add_markers(color=~당선, colors=c("#0D7440", "#2A88C5",  "#C10D0D"), opacity=0.6)

p2 = plot_ly(shared_elec, x = ~장애등급별.장애인현황, y = ~보육시설, legendgroup=~당선) %>%
  add_markers(color=~당선, colors=c("#0D7440", "#2A88C5",  "#C10D0D"), opacity=0.6)

subplot(p1,p2) %>%
  layout(xaxis = list(title = "소득"),
         yaxis = list(title = "노령화지수"),
         xaxis2 = list(title = "장애인현황"),
         yaxis2 = list(title = "보육시설"),
         height=400, width=850) %>%
  highlight("plotly_selected")

crosstalk은 서로 다른 종류의 위젯들 간에도 잘 작동합니다. 다만, 여러 위젯들이 요구하는 입력 형식이 다르기 때문에 약간의 조작이 더 필요합니다. 다음은 지도와 산점도, 테이블을 연결한 예제입니다. election 객체의 클래스는 SpatialPolygonsDataFame입니다. plot_ly 함수는 SpatialPolygonsDataFrame 클래스에 대해서 작동하지 않으므로, election@data를 별도로 공유 데이터로 만들어주면서 group=shared_elec$groupName() 인자를 전달했습니다. 즉 동일한 키에 의해 shared_elecshared_elec_df가 한 그룹으로 연결된 상태이고, 연동이 잘 이루어지는 것을 볼 수 있습니다. shared_elec_dt 역시 그룹을 지정하여 연동시켰습니다.

library(d3scatter)

shared_elec = SharedData$new(election)
shared_elec_df = SharedData$new(election@data, group=shared_elec$groupName())
shared_elec_dt = crosstalk::SharedData$new(election@data %>%
                 select(선거구,당선,읍면동명,소득,노령화지수), group=shared_elec$groupName())

DT::datatable(shared_elec_dt, width="100%")
bscols(
  d3scatter(data=shared_elec_df, x=~소득,y=~노령화지수,color=~당선, height=300, width="100%"),
  d3scatter(data=shared_elec_df, x=~장애등급별.장애인현황,y=~사업체수,color=~당선, height=300, width="100%")
)
leaflet(shared_elec) %>%
  addProviderTiles("CartoDB.Positron") %>% 
  setView(126.982, 37.5502, zoom=10) %>%
  addPolygons(
    fillColor = ~pal(당선),
    weight = 1,
    opacity = 1,
    color = "white",
    dashArray=3,
    fillOpacity = 0.5,
    label = lapply(labs, htmltools::HTML)) %>%
  addLegend(pal = pal, values = ~당선, opacity = 0.7, title = NULL, position = "bottomright") %>%
  addPolylines(
    data=election_gu,
    weight = 1.5,
    opacity = 1,
    color = "black")

3.2. Filtering

필터링은 말 그대로 특정 데이터 포인트를 필터링하는 기능이며, 크게 설명할 것이 없습니다. 예제와 함께 보겠습니다. filter_ 함수에 SharedData 넣어서 데이터에 맞는 형태의 필터 인풋을 만들어주면, 데이터를 공유하는 모든 위젯에 필터링이 적용됩니다.

bscols(
  list(filter_checkbox("당선", "당선", shared_elec_df, ~당선, inline=T),
       filter_slider("소득", "소득", shared_elec_df, ~소득, width="100%"),
       filter_select("동", "동", shared_elec_df, ~읍면동명)),
  d3scatter(shared_elec_df, ~소득, ~기초수급자인원수, ~당선, width="100%", height=250),
  d3scatter(shared_elec_df, ~소득, ~기초수급자인원수, ~당선, width="100%", height=250)
)
DT::datatable(shared_elec_dt, width="100%")

3.3. flexdashboard

마지막으로, 이번 장에서 다룬 내용들과 flexdashboard를 활용하여 대시보드를 구현하였습니다. 결과물과 코드는 여기에서 확인하실 수 있습니다.


참고문헌